package cn.chinamobile.remote.assistance.mobile.codec;

import java.io.IOException;
import java.nio.charset.Charset;
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.filterchain.BaseFilter;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.filterchain.NextAction;
import org.glassfish.grizzly.memory.MemoryManager;

public final class RemoteAssistanceCodec extends BaseFilter {

    private static final int RESPONSE_HEADER_SIZE = 9;

    private static final int DEVICE_INFO_HEADER_SIZE = 5;

    private static final String COLON = ":";

    private static final String CRLF = "\r\n";

    private static final byte[] SERVER_HEARTBEAT = ("hb" + CRLF).getBytes();

    private static final Charset CHARSET = Charset.forName("UTF-8");

    @Override
    public NextAction handleRead(final FilterChainContext ctx) throws IOException {

        final Buffer sourceBuffer = ctx.getMessage(); // Get the source buffer from the context
        final byte type = sourceBuffer.get(0);
        switch (type) {
            case 0:
                return handleMobileHeartBeat(ctx);
            case 1:
                return handleResponse(ctx);
            case 2:
                return handleDeviceInfo(ctx);
            case 4:
                return handleMobileClose(ctx);
            default:
                return handleErrorMessage(ctx);
        }
    }

    private NextAction handleMobileHeartBeat(final FilterChainContext ctx) {
        final Buffer sourceBuffer = ctx.getMessage(); // Get the source buffer from the context
        MobileHeartBeatMessage heartBeat = new MobileHeartBeatMessage(sourceBuffer.get());
        ctx.setMessage(heartBeat);
        sourceBuffer.tryDispose(); // We can try to dispose the buffer
        return ctx.getInvokeAction();
    }

    private NextAction handleResponse(final FilterChainContext ctx) {
        final Buffer sourceBuffer = ctx.getMessage(); // Get the source buffer from the context

        final int sourceBufferLength = sourceBuffer.remaining();
        if (sourceBufferLength < RESPONSE_HEADER_SIZE) { // If source buffer doesn't contain header
            return ctx.getStopAction(sourceBuffer); // Stop the filterchain processing and store sourceBuffer to be used next time
        }

        final int bodyLength = sourceBuffer.getInt(5); // Get the body length
        final int completeMessageLength = RESPONSE_HEADER_SIZE + bodyLength; // The complete message length
        if (sourceBufferLength < completeMessageLength) { // If the source message doesn't contain entire body
            return ctx.getStopAction(sourceBuffer); // Stop the filterchain processing and store sourceBuffer to be used next time
        }

        final byte messageType = sourceBuffer.get(); // Get the message type
        final int requestId = sourceBuffer.getInt(); // Get the request ID
        final byte[] body = new byte[sourceBuffer.getInt()]; // Allocate space to store the body
        sourceBuffer.get(body, 0, body.length); // Read body
        final String messageBody = new String(body, CHARSET);

        final ResponseMessage response = new ResponseMessage(messageType, requestId, messageBody);
        ctx.setMessage(response);

        // Check if the source buffer has more than 1 complete message. If yes - split up the first message and the remainder
        final Buffer remainder = sourceBufferLength > completeMessageLength ? sourceBuffer.split(completeMessageLength) : null;

        sourceBuffer.tryDispose(); // We can try to dispose the buffer

        return ctx.getInvokeAction(remainder); // Instruct FilterChain to store the remainder (if any) and continue execution
    }

    private NextAction handleDeviceInfo(final FilterChainContext ctx) {
        final Buffer sourceBuffer = ctx.getMessage(); // Get the source buffer from the context

        final int sourceBufferLength = sourceBuffer.remaining();
        if (sourceBufferLength < DEVICE_INFO_HEADER_SIZE) { // If source buffer doesn't contain header
            return ctx.getStopAction(sourceBuffer); // Stop the filterchain processing and store sourceBuffer to be used next time
        }

        final int bodyLength = sourceBuffer.getInt(1); // Get the body length
        final int completeMessageLength = DEVICE_INFO_HEADER_SIZE + bodyLength; // The complete message length
        if (sourceBufferLength < completeMessageLength) { // If the source message doesn't contain entire body
            return ctx.getStopAction(sourceBuffer); // Stop the filterchain processing and store sourceBuffer to be used next time
        }

        final byte messageType = sourceBuffer.get(); // Get the message type
        final byte[] body = new byte[sourceBuffer.getInt()]; // Allocate space to store the body
        sourceBuffer.get(body, 0, body.length); // Read body
        final String messageBody = new String(body, CHARSET);

        DeviceInfoMessage deviceInfo = new DeviceInfoMessage(messageType, messageBody);
        ctx.setMessage(deviceInfo);

        // Check if the source buffer has more than 1 complete message. If yes - split up the first message and the remainder
        final Buffer remainder = sourceBufferLength > completeMessageLength ? sourceBuffer.split(completeMessageLength) : null;

        sourceBuffer.tryDispose(); // We can try to dispose the buffer

        return ctx.getInvokeAction(remainder);
    }

    private NextAction handleMobileClose(final FilterChainContext ctx) {
        final Buffer sourceBuffer = ctx.getMessage(); // Get the source buffer from the context
        MobileCloseMessage closeMessage = new MobileCloseMessage(sourceBuffer.get()); // Get the message type
        ctx.setMessage(closeMessage);
        sourceBuffer.tryDispose(); // We can try to dispose the buffer
        return ctx.getInvokeAction();
    }

    private NextAction handleErrorMessage(final FilterChainContext ctx) {
        return ctx.getStopAction();
    }

    @Override
    public NextAction handleWrite(final FilterChainContext ctx) throws IOException {
        final Message message = ctx.getMessage(); // Get the source message to be written

        byte type = message.getMessageType();
        switch (type) {
            case 0:
                return handleServerHeartBeat(ctx);
            case 1:
                return handleRequest(ctx);
            default:
                return ctx.getInvokeAction();
        }
    }

    private NextAction handleServerHeartBeat(final FilterChainContext ctx) {
        final MemoryManager memoryManager = ctx.getConnection().getTransport().getMemoryManager();// Retrieve the memory manager

        final Buffer output = memoryManager.allocate(SERVER_HEARTBEAT.length); // Allocate the buffer of required size
        output.allowBufferDispose(true); // Allow Grizzly core to dispose the buffer, once it's written
        output.put(SERVER_HEARTBEAT); // send heartbeat content
        ctx.setMessage(output.flip()); // Set the Buffer as a context message

        return ctx.getInvokeAction(); // Instruct the FilterChain to call the next filter
    }

    private NextAction handleRequest(final FilterChainContext ctx) {
        final RequestMessage request = ctx.getMessage(); // Get the source message to be written

        final byte[] content = new StringBuilder()
                .append(request.getRequestId())
                .append(COLON)
                .append(request.getMessageBody())
                .append(CRLF).toString().getBytes(CHARSET);

        final MemoryManager memoryManager = ctx.getConnection().getTransport().getMemoryManager();// Retrieve the memory manag

        final Buffer output = memoryManager.allocate(content.length); // Allocate the buffer of required size
        output.allowBufferDispose(true); // Allow Grizzly core to dispose the buffer, once it's written
        output.put(content); // send request content
        ctx.setMessage(output.flip()); // Set the Buffer as a context message

        return ctx.getInvokeAction(); // Instruct the FilterChain to call the next filter
    }
}
